iT邦幫忙

2

kintone 外掛開發 ⑦ 透過外掛 proxy 執行外部請求 - 實作範例篇

  • 分享至 

  • xImage
  •  

在本系列上一篇文章 kintone 外掛開發 ⑥ 透過外掛 proxy 執行外部請求 - 入門概述篇 中,我們已介紹了 kintone 外掛 proxy 的概念與 API 用法。
這篇文章將透過一個實作範例,示範如何將 身份驗證資訊 安全地保存在 Plugin Proxy Config 中,並在前端程式碼中透過外掛 proxy 呼叫 API,同時避免敏感資訊暴露在瀏覽器端。

情境說明

假設我們要在應用程式中實作一個 自動編碼 功能:

  • 編碼的前綴為記錄建立日期(格式:yymmdd)。
  • 後綴為四位數流水號,例如:250808-0001。
  • 每天的流水號從 0001 開始遞增。
  • 保存編號的欄位為單行文字方塊,欄位名稱與代碼均為「申請單號」。

在這個應用中,記錄具有權限限制(例如:只有建立者與審核者可以查看),如果直接用 Session 驗證 呼叫 REST API,會因權限不足而無法查詢到「正確的最後一筆記錄」,導致編碼計算錯誤。

為了能跨越權限限制,我們需要透過 API 權杖 取得資料。然而,若將 API 權杖直接寫在客製化程式碼中,就會暴露在前端的 JavaScript 內,任何人只要打開瀏覽器開發者工具就能看見。

為了解決這個問題,我們可以將客製化功能改寫為外掛,並且透過以下方式來隱藏驗證資訊:

  1. 將 API 權杖保存在外掛的 Proxy 設定(setProxyConfig)。
  2. 前端呼叫 kintone.plugin.app.proxy() 時,自動合併並帶入已保存的權杖。

💡 外掛專案的建立與基本架構,請參考本系列 ① ~ ④ 篇:

程式碼範例與解說

使用套件

本範例會搭配以下兩個套件,分別用於外掛設定畫面與前端客製化程式碼中:本範例使用以下套件:

  1. kintone UI Component:
    用於外掛設定畫面,快速建立輸入框、按鈕等 UI 元件,並保持與 kintone 原生介面風格一致。

    CDN URL:

    https://unpkg.com/kintone-ui-component/umd/kuc.min.js
    
  2. Luxon:
    用於前端客製化程式碼中處理日期與時間,例如取得當天日期、格式化為 yymmdd 前綴等。

    CDN URL:

    https://js.cybozu.com/luxon/3.7.1/luxon.min.js
    

外掛設定畫面(config.html)

在設定畫面中建立一個容器區塊,供 JavaScript 動態產生 UI 元件並插入:

<div id="plugin-setting-container"></div>

外掛設定畫面(config.js)

此程式負責外掛設定頁面的行為邏輯,提供管理者輸入 API 權杖,並將其安全地保存至 Plugin Proxy Config 中。

((PLUGIN_ID) => {
  'use strict'

  // 建立 UI 元件
  const tokenInput = new Kuc.Text({
    label: 'API權杖',
    value: ''
  })

  const saveButton = new Kuc.Button({
    text: '保存',
    type: 'submit'
  })
  const cancelButton = new Kuc.Button({
    text: '取消',
    type: 'normal'
  })

  const buttonGroup = document.createElement('div')
  buttonGroup.style.display = 'flex'
  buttonGroup.style.justifyContent = 'flex-end'
  buttonGroup.style.gap = '8px'
  buttonGroup.style.paddingTop = '16px'
  buttonGroup.style.marginTop = '60px'
  buttonGroup.style.borderTop = '1px solid #e4e4e4'
  buttonGroup.appendChild(saveButton)
  buttonGroup.appendChild(cancelButton)

  const container = document.querySelector('#plugin-setting-container')
  container.appendChild(tokenInput)
  container.appendChild(buttonGroup)

  // 保存外掛設定
  const apiUrl = kintone.api.url('/k/v1/records')
  saveButton.addEventListener('click', () => {
    // 將 API 權杖保存到 Plugin Proxy Config 的標頭中
    const proxyHeaders = { 'X-Cybozu-API-Token': tokenInput.value }

    // 將呼叫 REST API 時的參數 "app"(應用程式 ID)預先設定至 Plugin Proxy Config 中
    const proxyData = { app: kintone.app.getId() }

    // 呼叫 kintone.plugin.app.setProxyConfig 來保存設定
    kintone.plugin.app.setProxyConfig(apiUrl, 'GET', proxyHeaders, proxyData)
  })

  // 返回按鈕
  cancelButton.addEventListener('click', () => {
    window.location.href = '../../' + kintone.app.getId() + '/plugin/'
  })

  // 從 Plugin Proxy Config 中讀取已保存的 API 權杖
  const pluginProxyConfig = kintone.plugin.app.getProxyConfig(apiUrl, 'GET')
  const savedToken = pluginProxyConfig?.headers['X-Cybozu-API-Token'] || ''
  tokenInput.value = savedToken

})(kintone.$PLUGIN_ID)

程式碼重點說明

建立輸入元件

使用 kintone UI Component 建立文字輸入框(tokenInput)供輸入 API 權杖。

使用 kintone.plugin.app.setProxyConfig() 儲存設定
kintone.plugin.app.setProxyConfig(url, method, headers, data, successCallback)

註:data 參數為請求主體(body)內容。

在本範例中,setProxyConfig() 用來將 API 權杖保存到 Plugin Proxy Config 的 headers 中,同時將呼叫 REST API 所需的 app(應用程式 ID)預先存入 data,方便後續請求直接使用。
另外,透過 getProxyConfig() 可以取得已保存的請求設定,再從 headers 中讀取先前儲存的 API 權杖,並在使用者再次回到外掛設定頁面時,自動填入輸入框,免去重複輸入的麻煩。

⚠️ 注意:請勿將 API 權杖以 setConfig() 儲存,因為在前端可以透過 getConfig() 直接讀取內容,這樣就失去了使用 setProxyConfig() 隱藏敏感資訊的意義。

💡 關於 setProxyConfig 在 GET/DELETE 方法下的特殊行為

一般來說,GET 請求的參數應放在請求網址中,而不是請求主體(body)。
不過 setProxyConfig() 在方法為 GET 或 DELETE 時,會自動將 data 中的物件內容轉換為 Query String,並與前端呼叫 plugin.proxy() 時的網址合併。

以本範例來看,Plugin Proxy Config 保存的設定如下:

  • url:https://{DOMAIN}.cybozu.com/k/v1/records.json
  • method:'GET'
  • headers:{ 'X-Cybozu-API-Token': API_TOKEN }
  • data: { app: 1 } (假設應用程式ID為 1 )

前端呼叫 kintone.plugin.app.proxy() 時帶入的參數:

  • url:https://{DOMAIN}.cybozu.com/k/v1/records.json?query={查詢條件}
  • method:'GET'
  • headers:{}
  • data: {}

實際發送的請求(由系統自動合併):

  • url:https://{DOMAIN}.cybozu.com/k/v1/records.json?query={查詢條件}&app=1
  • method:'GET'
  • headers:{ 'X-Cybozu-API-Token': API_TOKEN }
  • data: {}

⚠️ 注意:只有 setProxyConfig() 在 GET/DELETE 方法下,會自動將 data 轉為 Query String。kintone.plugin.app.proxy() 本身並不會進行這個轉換。

前端客製化程式碼(customize.js)

實際在應用程式中執行自動編碼功能,透過 plugin.proxy() 使用預存的請求資料來呼叫 kintone REST API,來取得編碼所需的最新一筆記錄,並且完成編碼邏輯。

((PLUGIN_ID) => {
  'use strict'

  const { DateTime } = luxon
  const apiUrl = kintone.api.url('/k/v1/records')

  // 禁止編輯「申請單號」欄位
  kintone.events.on([
    'app.record.create.show', 'mobile.app.record.create.show',
    'app.record.edit.show', 'mobile.app.record.edit.show', 'app.record.index.edit.show'
  ], event => {
    const applyNumber = event.record['申請單號']
    if (applyNumber) {
      applyNumber.disabled = true
    }
    return event
  })

  // 新增並保存記錄時,透過 REST API 取得最後一筆資料,並自動產生「申請單號」
  kintone.events.on(['app.record.create.submit', 'mobile.app.record.create.submit'], async event => {
    const { record } = event
    const applyNumber = record['申請單號']

    try {
      // 取得當日的起始時間與結束時間
      const startTime = DateTime.now().startOf('day').toISO()
      const endTime = DateTime.now().endOf('day').toISO()

      // 查詢當日建立的最後一筆記錄
      const query = `建立時間 >= "${startTime}" and 建立時間 <= "${endTime}" order by $id desc limit 1`
      const reqUrl = `${apiUrl}?query=${encodeURIComponent(query)}`

      // 透過 Plugin Proxy 呼叫 REST API
      const [resBody, status, resHeader] = await kintone.plugin.app.proxy(PLUGIN_ID, reqUrl, 'GET', {}, {})
      const res = JSON.parse(resBody)
      if (status !== 200) throw res

      const lastRecord = res.records[0]

      // 產生申請單號
      const lastApplyNumber = lastRecord ? lastRecord['申請單號']?.value : null
      const prefix = DateTime.now().toFormat('yyMMdd')

      if (!lastApplyNumber) {
        applyNumber.value = `${prefix}-0001`
      } else {
        const lastNumber = lastApplyNumber.slice(-4)
        const nextNumber = String(Number(lastNumber) + 1).padStart(4, '0')
        applyNumber.value = `${prefix}-${nextNumber}`
      }

    } catch (error) {
      console.error(error)
      event.error = error.message
    }

    return event
  })

})(kintone.$PLUGIN_ID)

程式碼重點說明

使用 kintone.plugin.app.proxy() 呼叫 API
kintone.plugin.app.proxy(pluginId, url, method, headers, data, successCallback, failureCallback)

此方法會在符合相同 應用程式、外掛 ID、HTTP 方法、URL 前綴 等條件時,自動將 setProxyConfig() 預先儲存的 headers 與 data 合併到請求中,並由 kintone 伺服器發送實際的 API 請求,再將結果回傳至前端。
由於整個請求是在伺服器端完成,前端無法直接看到完整的請求內容,因此能有效隱藏 API 權杖等敏感資訊。

查詢條件與 Query String 的組成

在產生請求網址時,先以 REST API 的 query 參數指定篩選條件(本範例為「當日建立的最後一筆記錄」),並使用 encodeURIComponent() 進行編碼,以確保 URL 格式正確。
最終組成的請求網址為:

https://{DOMAIN}/k/v1/records.json?query={查詢條件}

由於呼叫 plugin.app.proxy() 時帶入的 Plugin ID、方法(GET)、以及 URL 前綴(https://{DOMAIN}/k/v1/records.json)與先前在外掛設定畫面中用 setProxyConfig() 保存的資訊相符,因此系統會自動:

  1. 將儲存在 Plugin Proxy Config 中的 API 權杖加入 headers。
  2. 將儲存在 Plugin Proxy Config 中的 data(此處為 app: 應用程式ID)轉換成 Query String,並附加到請求網址末端。

結語

這次的範例帶大家體驗了 Plugin Proxy 的操作流程,也實際展現了它在 kintone 環境中保護敏感資訊的效果。無論是呼叫外部 API,或是在 kintone 內部系統中跨權限查詢資料,都能透過這個方式安全地傳遞驗證資訊,降低憑證外洩的風險。在開發需要處理高權限或敏感資料的功能時,不妨考慮套用這個模式,讓功能實現與安全性兼顧。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言